Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[6주차] Reddigg 미션 제출합니다. #16

Open
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

leejin-rho
Copy link
Member

[6주차]

[배포링크]

next-netflix-18th

[Key Questions]

정적 라우팅(Static Routing)/동적 라우팅(Dynamic Routing)이란?

- 정적라우팅
정적 라우팅이란, 고정된 페이지로 라우팅하는 것. /home 페이지가 대표적인 예시로, 접속시 랜더링 페이지 후 home 페이지로 라우팅된다. 이러한 방식으로 이동하는 것을 정적 라우팅한다고 표현한다. 자연스럽게 우리가 만든 파일 이름을 주소로 라우팅되는 것이 정적 라우팅이다.

- 동적라우팅
동적 라우팅은 가변적인 페이지로 라우팅하는 것. 영화 디테일 상세보기 같은 것이 대표적인 예시로, 실제 소스코드는 한 페이지지만 movie id에 따라서 주소가 변경된다. 대괄호를 이용해 동적 라우팅을 할 수 있는데, [movieId] 로 파일명을 만들어주고, pathname: /movie-detail/${movie.id}, 로 설정하여 라우팅이 가능하다.

성능 최적화를 위해 사용한 방법

무한 스크롤 구현을 위한 IntersectionObserver 사용→ 사용자가 스크롤을 맨 아래로 내리면 추가 데이터를 불러오는 무한 스크롤 기능을 구현했다. 전체데이터를 불러오기보다, 스크롤 이벤트를 감지하여 필요시에 api 호출을 하였다.

Copy link

@rmdnps10 rmdnps10 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tailwindcss 적용과, 무한 스크롤 적용 방식 등 리뷰하면서 많이 배워갔습니다 :)
이번 주 과제 고생많으셨습니다~

Comment on lines 71 to 72
<ImageBox className="pl-[0.75rem] w-full flex flex-col">
<h3 className="pl-[0.25rem] text-white mt-[14px] mb-[1.44rem] text-popular font-bold">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next에서 CSS적용할 때, SSR 방식으로 구현하는 점을 생각하면 Tailwind css 사용하는 게 제일 좋은 방법인 거 같아요:) 막상도입하는데 쉽지 않으셨을 거 같은데, 고생많으셨습니다!

Comment on lines +63 to +68
<Link
href={{
pathname: `/movie-detail/${movie.id}`,
query: { path: movie.backdrop_path, overview: movie.overview },
}}
>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Link 태그에서 이런 식으로 쿼리파라미터를 지정해서 넘겨줄 수 있는 점 배워갑니다!
찾아보니까 데이터 노출을 방지하기 위해 as 라는 속성을 이용하면 URL 를 마스킹 할 수도 있다고 해서 참고하시면 좋을 거 같아요!
query데이터 이동방법

Copy link
Member Author

@leejin-rho leejin-rho Nov 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as를 쓰니까 왜인지 useSearchParam이 읽지를 못하더라구요... ㅠㅠ

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 배워갑니다!

Comment on lines 24 to 26
const [topRatedData] = await Promise.all([
tmdbApi.get(requests.fetchTopRated),
]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 바꿀 수 있을 거 같습니다!

Suggested change
const [topRatedData] = await Promise.all([
tmdbApi.get(requests.fetchTopRated),
]);
const topRatedData = await tmdbApi.get(requests.fetchTopRated);

Comment on lines 28 to 30
} catch (error) {
console.error("Failed to fetch movies:", error);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try-cath문으로 에러처리까지 고려하신 점 잘하신 거 같아요:)

};

const filteredMovies = topRated.filter((movie) =>
includesChosung(movie.title, searchQuery),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수 이름이 엣지넘치네요 ㅎㅎㅋㅋㅋㅋㅋㅋㅋㅋ 완전 직관적이에요

Comment on lines 109 to 114
<Link
href={{
pathname: `/movie-detail/${movie.id}`,
query: { path: movie.backdrop_path, overview: movie.overview },
}}
>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검색 페이지에서 클릭하면 상세 화면으로 이동하게 구현하신 점 좋아요 👍

Comment on lines +32 to +44
const [
nowPlayingData,
topRatedData,
popularData,
trendingData,
horrorData,
] = await Promise.all([
tmdbApi.get(requests.fetchNowPlaying),
tmdbApi.get(requests.fetchTopRated),
tmdbApi.get(requests.fetchPopular),
tmdbApi.get(requests.fetchTrending),
tmdbApi.get(requests.fetchHorrorMovies),
]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러 비동기 작업들을 한번에 이행하기 위해서 Promise.all을 적절히 사용하신 것 같습니다:)

Comment on lines +57 to +63
//현재 페이지 변경-> 영화 데이터 불러옴
useEffect(() => {
if (currentPage > 1 && hasMore)
fetchMoviesFromPage(currentPage).then((newMovies) => {
setTopRated((prevMovies) => [...prevMovies, ...newMovies]);
});
}, [currentPage]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentPage의 상태에 따라 fetch를 하여 topRatedMovies 를 반영하여 무한 스크롤을 구현하신 점 멋있습니다 👍

Comment on lines +87 to +98
const lastMovieElementRef = useCallback(
(node: Element | null) => {
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setCurrentPage((prevPage) => prevPage + 1);
}
});
if (node) observer.current.observe(node);
},
[isLoading],
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굉장히 여러가지를 신경써서 코드를 작성하신 것 같아요~ useCallback을 통해 isLoading이 변경되지 않으면 함수가 생성되지 않게 최적화를 진행하시고 IntersectionObserver을 통해 마지막 요소가 화면에서 교차될 때 currentPage의 상태를 변하게 하여 무한 스크롤 구현하신 점 배워갑니다!

Copy link

@flowerseok flowerseok left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

바쁘실텐데 고생 많이 하셨습니다.
재미있고 도움되는 코드 였습니다. 오늘도 역시 많이 배워갑니다 ~~~!!

tmdbApi.get(requests.fetchPopular),
tmdbApi.get(requests.fetchTrending),
tmdbApi.get(requests.fetchHorrorMovies),
]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 처음보는 promise.all이네요! 깊이가 깊지 않다보니, 사용하면서 배우는 경우가 많은데, 비동기 작업을 이행하는 것 공부해보고 싶네요 ~~

const fetchMoviesFromPage = async (page: number) => {
setIsLoading(true);
const response = await tmdbApi.get<{
results: Movie[];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 axios와 async/await의 조합을 사용하셨는데, axios는 HTTP 요청을 간편하게 만들어주고, async/await를 통해 이러한 비동기 요청을 동기적으로 처리할 수 있게 해주는 게 맞나요? 혹시 이러한 조합을 선택하신 이유가 있을까요? 저는 사용하지 않아서, 제가 이해한 것이 맞는지, 추가로 배울 점이 있다면 알고 싶어요 ~~~

Comment on lines +63 to +68
<Link
href={{
pathname: `/movie-detail/${movie.id}`,
query: { path: movie.backdrop_path, overview: movie.overview },
}}
>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 배워갑니다!

src/app/search/page.tsx Show resolved Hide resolved
Copy link

@oooppq oooppq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한 주동안 수고하셨습니다~~ 전체적으로 디테일과 최적화를 고려하셔서 좋은 결과물이 나온 것 같아요!!

Comment on lines +28 to +58
useEffect(() => {
const fetchMovies = async () => {
try {
// API 요청을 한번에 보냄
const [
nowPlayingData,
topRatedData,
popularData,
trendingData,
horrorData,
] = await Promise.all([
tmdbApi.get(requests.fetchNowPlaying),
tmdbApi.get(requests.fetchTopRated),
tmdbApi.get(requests.fetchPopular),
tmdbApi.get(requests.fetchTrending),
tmdbApi.get(requests.fetchHorrorMovies),
]);

// 상태 한번에 업데이트
setNowPlaying(nowPlayingData.data.results);
setTopRated(topRatedData.data.results);
setPopular(popularData.data.results);
setTrending(trendingData.data.results);
setHorror(horrorData.data.results);
} catch (error) {
console.error("Failed to fetch movies:", error);
}
};

fetchMovies();
}, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ssr을 공부하고 있는 만큼 server side에서 fetching하는 방법을 사용했어도 좋았겠지만, 기능상에는 큰 문제가 없기 때문에 잘 구현하신 것 같아요~~ 다만, 해당 fetching 작업이 client side에서 발생하기 때문에, api를 통해 영화 정보를 가져오는 시간동안 해당 페이지의 text들만 미리 렌더링되어 화면이 버벅되는 모습을 볼 수 있어요!

스크린샷 2023-11-19 오후 2 45 58

이런 식으로요! 이를 막기 위해서 state를 통해 loading flag을 설정해주거나, 스켈레톤 컴포넌트를 구성해두는 방법이 있을 것 같아요 한 번 생각해보시면 좋을 것 같아요~

src/app/search/page.tsx Show resolved Hide resolved
Comment on lines +90 to +95
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setCurrentPage((prevPage) => prevPage + 1);
}
});
if (node) observer.current.observe(node);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

따로 라이브러리 사용하지 않고 intersection observer을 활용해서 직접 구현하신거 대단해요~~

Comment on lines +126 to +137
<div
key={movie.id}
ref={
filteredMovies.length === index + 1
? lastMovieElementRef
: undefined
}
>
{/* {renderMovieLists(filteredMovies)} */}
{renderMovieLists([movie])}
</div>
))}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요부분에서 renderMovieLists라는 함수를 사용해주셨는데, 원래 해당 함수는 리스트 형태의 영화 데이터를 렌더링해주기 위해 만든 함수 같아요! 근데 아마 리팩토링하면서 변화가 있었던 것 같아요, 이렇게 바꾸신 김에 해당 함수를 사용하지 않고 그냥 따로 컴포넌트로 빼서 사용하는 것이 가독성에 좋을 것 같아요! 컴포넌트로 구성한다면 react memo를 통해 memoization을 할 수도 있으니까요!

isActive: boolean;
children: JSX.Element;
text: string;
onClick?: () => void;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onClick 이벤트 prop을 전달하는 과정에서 서로 다른 onClick 이벤트가 같이 정의되어 있을 수도 있기 때문에 용도에 맞게 네이밍해주시면 더 좋을 것 같아요~~ 예를 들어, handleClickNavButton 과 같은 형태로요!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이유는 모르겠는데, navbar의 현재 페이지의 아이콘을 클릭해서(예를 들어, 현재 home인데, home 버튼을 눌렀을 때) url의 이동이 없는데도 불구하고, 페이지가 리렌더링 되는 것 같아요.. 계속 이유를 찾아보려고 했는데 잘 모르겠네요ㅜ 저희꺼를 포함해서 다른분들꺼는 같은페이지 버튼을 눌러도 리렌더링이 발생하지 않는데 왜 그런 것인지 함 찾아보면 좋을 것 같습니다!

Copy link

@kyuhho kyuhho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한주동안 고생 많으셨습니다~~ 코드의 디테일이 매우 좋네요 👍👍

Comment on lines +22 to +26
const [topRated, setTopRated] = useState<Movie[]>([]);
const [popular, setPopular] = useState<Movie[]>([]);
const [nowPlaying, setNowPlaying] = useState<Movie[]>([]);
const [trending, setTrending] = useState<Movie[]>([]);
const [horror, setHorror] = useState<Movie[]>([]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희는 javascript를 사용했는데, typescript로 movie interface 따로 선언하여 상위 컴포넌트에서 fetch 데이터를 한번에 관리해주는게 깔끔합니다!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utils 함수에 render 관련 함수들을 모아준게 가독성이 좋습니다!!

);
};

const NavBarContainer = styled.nav`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

배포한 사이트에서 navbar가 살짝 엇나가 보이는 문제가 있는거 같습니다!
image

Comment on lines +32 to +43
const [
nowPlayingData,
topRatedData,
popularData,
trendingData,
horrorData,
] = await Promise.all([
tmdbApi.get(requests.fetchNowPlaying),
tmdbApi.get(requests.fetchTopRated),
tmdbApi.get(requests.fetchPopular),
tmdbApi.get(requests.fetchTrending),
tmdbApi.get(requests.fetchHorrorMovies),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise.all 사용해서 한번에 get 요청을 할 수 있다는 걸 배워갑니다~!

Comment on lines +87 to +98
const lastMovieElementRef = useCallback(
(node: Element | null) => {
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setCurrentPage((prevPage) => prevPage + 1);
}
});
if (node) observer.current.observe(node);
},
[isLoading],
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에서 currentPage를 state로 관리하며 IntersectionObserver를 이용하여 무한스크롤 구현하신게 인상 깊습니다!! 덕분에 새로 하나 배워갑니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants